/*
 * io.c
 *
 * Copyright (C) 2019 Aleksandar Andrejevic <theflash@sdf.lonestar.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <unistd.h>
#include "crt_priv.h"
#include "io_priv.h"

static list_entry_t open_files;

FILE *stdin  = NULL;
FILE *stdout = NULL;
FILE *stderr = NULL;

int __CRT_PUBLIC(fileno_unlocked)(FILE *stream)
{
    if (stream->fd == -1) __CRT_PUBLIC(errno) = EBADF;
    return stream->fd;
}

int __CRT_PUBLIC(fileno)(FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = __CRT_PUBLIC(fileno_unlocked)(stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

static int __CRT_PRIVATE(fflush_stream)(FILE *stream)
{
    if (!stream->buffer) return EOF;

    if (!(stream->flags & FILE_BUFFER_DIR))
    {
        stream->buffer_start = stream->buffer_end = 0;
        stream->flags &= ~FILE_BUFFER_FULL;
        return 0;
    }

    if (stream->fd == -1) return 0;

    if (stream->buffer_start < stream->buffer_end)
    {
        ssize_t written = __CRT_PUBLIC(write)(stream->fd, &stream->buffer[stream->buffer_start], stream->buffer_end - stream->buffer_start);
        if (written < 0) return EOF;

        stream->buffer_start += written;
        stream->buffer_start %= stream->buffer_size;
        if (written) stream->flags &= ~FILE_BUFFER_FULL;
    }
    else
    {
        if (stream->buffer_start < stream->buffer_size)
        {
            ssize_t written = __CRT_PUBLIC(write)(stream->fd, &stream->buffer[stream->buffer_start], stream->buffer_size - stream->buffer_start);
            if (written < 0) return EOF;

            stream->buffer_start += written;
            stream->buffer_start %= stream->buffer_size;
            if (written) stream->flags &= ~FILE_BUFFER_FULL;
        }

        if (stream->buffer_end)
        {
            ssize_t written =__CRT_PUBLIC(write)(stream->fd, stream->buffer, stream->buffer_end);
            if (written < 0) return EOF;

            stream->buffer_start += written;
            stream->buffer_start %= stream->buffer_size;
            if (written) stream->flags &= ~FILE_BUFFER_FULL;
        }
    }

    return 0;
}

int __CRT_PUBLIC(fflush_unlocked)(FILE *stream)
{
    if (stream) return __CRT_PRIVATE(fflush_stream)(stream);

    list_entry_t *entry;
    int result = 0;

    for (entry = open_files.next; entry != &open_files; entry = entry->next)
    {
        if (__CRT_PRIVATE(fflush_stream)(CONTAINER_OF(entry, struct __crt_file, link)) == EOF)
        {
            result = EOF;
        }
    }

    return result;
}

int __CRT_PUBLIC(fflush)(FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = __CRT_PUBLIC(fflush_unlocked)(stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

int __CRT_PUBLIC(setvbuf)(FILE *stream, char *buf, int mode, size_t size)
{
    int ret = EOF;
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);

    switch (mode)
    {
    case _IONBF:
        if (__CRT_PUBLIC(fflush_unlocked)(stream) == 0)
        {
            stream->buffer = NULL;
            stream->buffer_size = 0;
            ret = 0;
        }
        break;

    case _IOLBF:
        if (!buf || __CRT_PUBLIC(fflush_unlocked)(stream) == 0)
        {
            if (buf)
            {
                stream->buffer = buf;
                stream->buffer_size = size;
            }

            stream->flags |= FILE_BUFFER_LINE;
            ret = 0;
        }
        break;

    case _IOFBF:
        if (!buf || __CRT_PUBLIC(fflush_unlocked)(stream) == 0)
        {
            if (buf)
            {
                stream->buffer = buf;
                stream->buffer_size = size;
            }

            stream->flags &= ~FILE_BUFFER_LINE;
            ret = 0;
        }
        break;
    }

    syscall_release_mutex(stream->mutex);
    return ret;
}

void __CRT_PUBLIC(setbuf)(FILE *stream, char *buf)
{
    __CRT_PUBLIC(setvbuf)(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
}

void __CRT_PUBLIC(setbuffer)(FILE *stream, char *buf, size_t size)
{
    __CRT_PUBLIC(setvbuf)(stream, buf, buf ? _IOFBF : _IONBF, size);
}

void __CRT_PUBLIC(setlinebuf)(FILE *stream)
{
    __CRT_PUBLIC(setvbuf)(stream, NULL, _IOLBF, 0);
}

inline int __CRT_PUBLIC(fgetc_unlocked)(FILE *stream)
{
    char c;

    if (!(stream->flags & FILE_READ))
    {
        __CRT_PUBLIC(errno) = EPERM;
        return EOF;
    }

    if (stream->buffer)
    {
        if (stream->flags & FILE_BUFFER_DIR)
        {
            __CRT_PUBLIC(fflush_unlocked)(stream);
            stream->flags &= ~FILE_BUFFER_DIR;
        }

        if (stream->buffer_start == stream->buffer_end && !(stream->flags & FILE_BUFFER_FULL))
        {
            if (stream->fd == -1) return EOF;

            if (stream->buffer_end < stream->buffer_size)
            {
                ssize_t amount = __CRT_PUBLIC(read)(stream->fd, &stream->buffer[stream->buffer_end], stream->buffer_size - stream->buffer_end);
                if (amount < 0) return EOF;

                stream->buffer_end += amount;
                stream->buffer_end %= stream->buffer_size;
                if (amount && (stream->buffer_start == stream->buffer_end)) stream->flags |= FILE_BUFFER_FULL;
            }

            if (stream->buffer_end < stream->buffer_start)
            {
                ssize_t amount = __CRT_PUBLIC(read)(stream->fd, &stream->buffer[stream->buffer_end], stream->buffer_start - stream->buffer_end);
                if (amount < 0) return EOF;

                stream->buffer_end += amount;
                stream->buffer_end %= stream->buffer_size;
                if (amount && (stream->buffer_start == stream->buffer_end)) stream->flags |= FILE_BUFFER_FULL;
            }
        }

        c = stream->buffer[stream->buffer_start];
        stream->buffer_start++;
        stream->buffer_end %= stream->buffer_size;

        stream->flags &= ~FILE_BUFFER_FULL;
    }
    else
    {
        if (__CRT_PUBLIC(read)(stream->fd, &c, 1) != 1) c = EOF;
    }

    return (int)((unsigned int)c);
}

int __CRT_PUBLIC(fgetc)(FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = __CRT_PUBLIC(fgetc_unlocked)(stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

inline int __CRT_PUBLIC(fputc_unlocked)(int c, FILE *stream)
{
    if (!(stream->flags & FILE_WRITE))
    {
        __CRT_PUBLIC(errno) = EPERM;
        return EOF;
    }

    if (stream->buffer)
    {
        if (!(stream->flags & FILE_BUFFER_DIR))
        {
            __CRT_PUBLIC(fflush_unlocked)(stream);
            stream->flags |= FILE_BUFFER_DIR;
        }

        if (stream->flags & FILE_BUFFER_FULL)
        {
            __CRT_PUBLIC(errno) = ENOSPC;
            return EOF;
        }

        stream->buffer[stream->buffer_end] = (uint8_t)c;
        stream->buffer_end++;
        stream->buffer_end %= stream->buffer_size;

        if (stream->buffer_start == stream->buffer_end)
        {
            stream->flags |= FILE_BUFFER_FULL;
        }

        if (stream->fd != -1 && ((stream->flags & FILE_BUFFER_FULL) || ((stream->flags & FILE_BUFFER_LINE) && (char)c == '\n')))
        {
            __CRT_PUBLIC(fflush_unlocked)(stream);
        }
    }
    else
    {
        if (__CRT_PUBLIC(write)(stream->fd, &c, 1) != 1) return EOF;
    }

    return 0;
}

int __CRT_PUBLIC(fputc)(int c, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = __CRT_PUBLIC(fputc_unlocked)(c, stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

char *__CRT_PUBLIC(fgets_unlocked)(char *s, int size, FILE *stream)
{
    int c;
    char *ptr = s;

    while (size-- > 0 && (c = __CRT_PUBLIC(fgetc_unlocked)(stream)) != EOF)
    {
        *ptr++ = (char)c;
        if (c == '\n') break;
    }

    return (s != ptr) ? s : NULL;
}

char *__CRT_PUBLIC(fgets)(char *s, int size, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    char *ret = __CRT_PUBLIC(fgets_unlocked)(s, size, stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

int __CRT_PUBLIC(fputs_unlocked)(const char *s, FILE *stream)
{
    const char *ptr = s;
    while (*ptr) if (__CRT_PUBLIC(fputc_unlocked)(*ptr++, stream) == EOF) return EOF;
    return 0;
}

int __CRT_PUBLIC(fputs)(const char *s, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);
    int ret = __CRT_PUBLIC(fputs_unlocked)(s, stream);
    syscall_release_mutex(stream->mutex);
    return ret;
}

int __CRT_PUBLIC(ungetc)(int c, FILE *stream)
{
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);

    if (stream->flags & FILE_BUFFER_DIR)
    {
        __CRT_PUBLIC(fflush_unlocked)(stream);
        stream->flags &= ~FILE_BUFFER_DIR;
    }

    if (stream->buffer_start == 0) stream->buffer_start = stream->buffer_size;
    stream->buffer_start--;

    stream->buffer[stream->buffer_start] = (uint8_t)c;

    syscall_release_mutex(stream->mutex);
    return c;
}

char *__CRT_PUBLIC(gets)(char *s)
{
    char *ptr = s;
    int c;
    while ((c = __CRT_PUBLIC(getchar)()) != EOF) *ptr++ = (char)c;
    return ptr != s ? s : NULL;
}

int __CRT_PUBLIC(puts)(const char *s)
{
    if (__CRT_PUBLIC(fputs)(s, __CRT_PUBLIC(stdout)) == EOF) return EOF;
    if (__CRT_PUBLIC(fputc)('\n', __CRT_PUBLIC(stdout)) == EOF) return EOF;
    return 0;
}

FILE *__CRT_PUBLIC(fdopen)(int fd, const char *mode)
{
    FILE *stream = (FILE*)__CRT_PUBLIC(malloc)(sizeof(FILE) + BUFSIZ);

    if (stream == NULL)
    {
        __CRT_PUBLIC(errno) = ENOMEM;
        return NULL;
    }

    sysret_t status = syscall_create_mutex(NULL, TRUE, &stream->mutex);
    if (status != ERR_SUCCESS)
    {
        __CRT_PUBLIC(free)(stream);
        __CRT_PUBLIC(errno) = __CRT_PRIVATE(translate_error)(status);
        return NULL;
    }

    stream->flags = 0;

    const char *ptr;
    for (ptr = mode; *ptr; ptr++)
    {
        switch (*ptr)
        {
        case 'r':
            stream->flags |= FILE_READ;
            break;
        case 'w':
            stream->flags |= FILE_WRITE;
            break;
        case 'a':
            stream->flags |= FILE_APPEND;
            break;
        case '+':
            stream->flags |= FILE_READ | FILE_WRITE;
            break;
        }
    }

    stream->fd = fd;

    char *buffer = (char*)((uintptr_t)stream + sizeof(FILE));
    __CRT_PUBLIC(setbuf)(stream, buffer);
    return stream;
}

FILE *__CRT_PUBLIC(fmemopen)(void *buf, size_t size, const char *mode)
{
    FILE *stream = (FILE*)__CRT_PUBLIC(malloc)(sizeof(FILE));
    if (stream == NULL)
    {
        __CRT_PUBLIC(errno) = ENOMEM;
        return NULL;
    }

    sysret_t status = syscall_create_mutex(NULL, TRUE, &stream->mutex);
    if (status != ERR_SUCCESS)
    {
        __CRT_PUBLIC(free)(stream);
        __CRT_PUBLIC(errno) = __CRT_PRIVATE(translate_error)(status);
        return NULL;
    }

    stream->flags = 0;

    const char *ptr;
    for (ptr = mode; *ptr; ptr++)
    {
        switch (*ptr)
        {
        case 'r':
            stream->flags |= FILE_READ;
            break;
        case 'w':
            stream->flags |= FILE_WRITE;
            break;
        case 'a':
            stream->flags |= FILE_APPEND;
            break;
        case '+':
            stream->flags |= FILE_READ | FILE_WRITE;
            break;
        }
    }

    stream->fd = -1;
    stream->buffer = buf;
    stream->buffer_size = size;
    stream->buffer_start = stream->buffer_end = 0;

    list_append(&open_files, &stream->link);
    return stream;
}

FILE *__CRT_PUBLIC(fopen)(const char *pathname, const char *mode)
{
    int open_flags = 0;

    const char *ptr;
    for (ptr = mode; *ptr; ptr++)
    {
        switch (*ptr)
        {
        case 'r':
            open_flags = (open_flags & ~3) | O_RDONLY;
            break;
        case 'w':
            open_flags = (open_flags & ~3) | O_WRONLY;
            break;
        case 'a':
            open_flags |= O_APPEND;
            break;
        case '+':
            open_flags = (open_flags & ~3) | O_RDWR;
            break;
        }
    }

    int fd = __CRT_PUBLIC(open)(pathname, open_flags);
    if (fd < 0) return NULL;

    FILE *stream = __CRT_PUBLIC(fdopen)(fd, mode);
    if (!stream) __CRT_PUBLIC(close)(fd);
    return NULL;
}

int __CRT_PUBLIC(fclose)(FILE *stream)
{
    __CRT_PUBLIC(fflush)(stream);
    list_remove(&stream->link);
    syscall_wait_mutex(stream->mutex, NO_TIMEOUT);

    if (stream->fd) __CRT_PUBLIC(close)(stream->fd);

    syscall_close_object(stream->mutex);
    __CRT_PUBLIC(free)(stream);
    return 0;
}
